/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.dubbo.common.utils;

import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Proxy;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.IdentityHashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.TreeMap;
import java.util.WeakHashMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.function.Consumer;
import java.util.function.Supplier;

import org.apache.dubbo.common.config.ConfigurationUtils;
import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.logger.ErrorTypeAwareLogger;
import org.apache.dubbo.common.logger.LoggerFactory;

import static org.apache.dubbo.common.constants.LoggerCodeConstants.COMMON_REFLECTIVE_OPERATION_FAILED;
import static org.apache.dubbo.common.utils.ClassUtils.isAssignableFrom;

/**
 * PojoUtils. Travel object deeply, and convert complex type to simple type.
 * <p/>
 * Simple type below will be remained:
 * <ul>
 * <li> Primitive Type, also include <b>String</b>, <b>Number</b>(Integer, Long), <b>Date</b>
 * <li> Array of Primitive Type
 * <li> Collection, eg: List, Map, Set etc.
 * </ul>
 * <p/>
 * Other type will be covert to a map which contains the attributes and value pair of object.
 * <p>
 * TODO: exact PojoUtils to scope bean
 */
public class PojoUtils {

    private static final ErrorTypeAwareLogger                                   logger                = LoggerFactory
        .getErrorTypeAwareLogger(PojoUtils.class);
    private static final ConcurrentMap<Class<?>, ConcurrentMap<String, Method>> NAME_METHODS_CACHE    = new ConcurrentHashMap<>();
    private static final ConcurrentMap<Class<?>, ConcurrentMap<String, Field>>  CLASS_FIELD_CACHE     = new ConcurrentHashMap<Class<?>, ConcurrentMap<String, Field>>();

    private static final ConcurrentMap<String, Object>                          CLASS_NOT_FOUND_CACHE = new ConcurrentHashMap<String, Object>();

    private static final Object                                                 NOT_FOUND_VALUE       = new Object();
    private static final boolean                                                GENERIC_WITH_CLZ      = Boolean
        .parseBoolean(ConfigurationUtils.getProperty(CommonConstants.GENERIC_WITH_CLZ_KEY, "true"));
    private static final List<Class<?>>                                         CLASS_CAN_BE_STRING   = Arrays
        .asList(Byte.class, Short.class, Integer.class, Long.class, Float.class, Double.class,
            Boolean.class, Character.class);

    public static Object[] generalize(Object[] objs) {
        Object[] dests = new Object[objs.length];
        for (int i = 0; i < objs.length; i++) {
            dests[i] = generalize(objs[i]);
        }
        return dests;
    }

    public static Object[] realize(Object[] objs, Class<?>[] types) {
        if (objs.length != types.length) {
            throw new IllegalArgumentException("args.length != types.length");
        }

        Object[] dests = new Object[objs.length];
        for (int i = 0; i < objs.length; i++) {
            dests[i] = realize(objs[i], types[i]);
        }

        return dests;
    }

    public static Object[] realize(Object[] objs, Class<?>[] types, Type[] gtypes) {
        if (objs.length != types.length || objs.length != gtypes.length) {
            throw new IllegalArgumentException("args.length != types.length");
        }
        Object[] dests = new Object[objs.length];
        for (int i = 0; i < objs.length; i++) {
            dests[i] = realize(objs[i], types[i], gtypes[i]);
        }
        return dests;
    }

    public static Object generalize(Object pojo) {
        return generalize(pojo, new IdentityHashMap<Object, Object>());
    }

    @SuppressWarnings("unchecked")
    private static Object generalize(Object pojo, Map<Object, Object> history) {
        if (pojo == null) {
            return null;
        }

        if (pojo instanceof Enum<?>) {
            return ((Enum<?>) pojo).name();
        }
        if (pojo.getClass().isArray()
            && Enum.class.isAssignableFrom(pojo.getClass().getComponentType())) {
            int len = Array.getLength(pojo);
            String[] values = new String[len];
            for (int i = 0; i < len; i++) {
                values[i] = ((Enum<?>) Array.get(pojo, i)).name();
            }
            return values;
        }

        if (ReflectUtils.isPrimitives(pojo.getClass())) {
            return pojo;
        }

        if (pojo instanceof LocalDate || pojo instanceof LocalDateTime
            || pojo instanceof LocalTime) {
            return pojo.toString();
        }

        if (pojo instanceof Class) {
            return ((Class) pojo).getName();
        }

        Object o = history.get(pojo);
        if (o != null) {
            return o;
        }
        history.put(pojo, pojo);

        if (pojo.getClass().isArray()) {
            int len = Array.getLength(pojo);
            Object[] dest = new Object[len];
            history.put(pojo, dest);
            for (int i = 0; i < len; i++) {
                Object obj = Array.get(pojo, i);
                dest[i] = generalize(obj, history);
            }
            return dest;
        }
        if (pojo instanceof Collection<?>) {
            Collection<Object> src = (Collection<Object>) pojo;
            int len = src.size();
            Collection<Object> dest = (pojo instanceof List<?>) ? new ArrayList<Object>(len)
                : new HashSet<Object>(len);
            history.put(pojo, dest);
            for (Object obj : src) {
                dest.add(generalize(obj, history));
            }
            return dest;
        }
        if (pojo instanceof Map<?, ?>) {
            Map<Object, Object> src = (Map<Object, Object>) pojo;
            Map<Object, Object> dest = createMap(src);
            history.put(pojo, dest);
            for (Map.Entry<Object, Object> obj : src.entrySet()) {
                dest.put(generalize(obj.getKey(), history), generalize(obj.getValue(), history));
            }
            return dest;
        }
        Map<String, Object> map = new HashMap<String, Object>();
        history.put(pojo, map);
        if (GENERIC_WITH_CLZ) {
            map.put("class", pojo.getClass().getName());
        }
        for (Method method : pojo.getClass().getMethods()) {
            if (ReflectUtils.isBeanPropertyReadMethod(method)) {
                ReflectUtils.makeAccessible(method);
                try {
                    map.put(ReflectUtils.getPropertyNameFromBeanReadMethod(method),
                        generalize(method.invoke(pojo), history));
                } catch (Exception e) {
                    throw new RuntimeException(e.getMessage(), e);
                }
            }
        }
        // public field
        for (Field field : pojo.getClass().getFields()) {
            if (ReflectUtils.isPublicInstanceField(field)) {
                try {
                    Object fieldValue = field.get(pojo);
                    if (history.containsKey(pojo)) {
                        Object pojoGeneralizedValue = history.get(pojo);
                        if (pojoGeneralizedValue instanceof Map
                            && ((Map) pojoGeneralizedValue).containsKey(field.getName())) {
                            continue;
                        }
                    }
                    if (fieldValue != null) {
                        map.put(field.getName(), generalize(fieldValue, history));
                    }
                } catch (Exception e) {
                    throw new RuntimeException(e.getMessage(), e);
                }
            }
        }
        return map;
    }

    public static Object realize(Object pojo, Class<?> type) {
        return realize0(pojo, type, null, new IdentityHashMap<Object, Object>());
    }

    public static Object realize(Object pojo, Class<?> type, Type genericType) {
        return realize0(pojo, type, genericType, new IdentityHashMap<Object, Object>());
    }

    private static class PojoInvocationHandler implements InvocationHandler {

        private Map<Object, Object> map;

        public PojoInvocationHandler(Map<Object, Object> map) {
            this.map = map;
        }

        @Override
        @SuppressWarnings("unchecked")
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            if (method.getDeclaringClass() == Object.class) {
                return method.invoke(map, args);
            }
            String methodName = method.getName();
            Object value = null;
            if (methodName.length() > 3 && methodName.startsWith("get")) {
                value = map.get(methodName.substring(3, 4).toLowerCase() + methodName.substring(4));
            } else if (methodName.length() > 2 && methodName.startsWith("is")) {
                value = map.get(methodName.substring(2, 3).toLowerCase() + methodName.substring(3));
            } else {
                value = map.get(methodName.substring(0, 1).toLowerCase() + methodName.substring(1));
            }
            if (value instanceof Map<?, ?> && !Map.class.isAssignableFrom(method.getReturnType())) {
                value = realize0((Map<String, Object>) value, method.getReturnType(), null,
                    new IdentityHashMap<Object, Object>());
            }
            return value;
        }
    }

    @SuppressWarnings("unchecked")
    private static Collection<Object> createCollection(Class<?> type, int len) {
        if (type.isAssignableFrom(ArrayList.class)) {
            return new ArrayList<Object>(len);
        }
        if (type.isAssignableFrom(HashSet.class)) {
            return new HashSet<Object>(len);
        }
        if (!type.isInterface() && !Modifier.isAbstract(type.getModifiers())) {
            try {
                return (Collection<Object>) type.newInstance();
            } catch (Exception e) {
                // ignore
            }
        }
        return new ArrayList<Object>();
    }

    private static Map createMap(Map src) {
        Class<? extends Map> cl = src.getClass();
        Map result = null;
        if (HashMap.class == cl) {
            result = new HashMap();
        } else if (Hashtable.class == cl) {
            result = new Hashtable();
        } else if (IdentityHashMap.class == cl) {
            result = new IdentityHashMap();
        } else if (LinkedHashMap.class == cl) {
            result = new LinkedHashMap();
        } else if (Properties.class == cl) {
            result = new Properties();
        } else if (TreeMap.class == cl) {
            result = new TreeMap();
        } else if (WeakHashMap.class == cl) {
            return new WeakHashMap();
        } else if (ConcurrentHashMap.class == cl) {
            result = new ConcurrentHashMap();
        } else if (ConcurrentSkipListMap.class == cl) {
            result = new ConcurrentSkipListMap();
        } else {
            try {
                result = cl.newInstance();
            } catch (Exception e) { /* ignore */
            }

            if (result == null) {
                try {
                    Constructor<?> constructor = cl.getConstructor(Map.class);
                    result = (Map) constructor.newInstance(Collections.EMPTY_MAP);
                } catch (Exception e) { /* ignore */
                }
            }
        }

        if (result == null) {
            result = new HashMap<Object, Object>();
        }

        return result;
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    private static Object realize0(Object pojo, Class<?> type, Type genericType,
                                   final Map<Object, Object> history) {
        if (pojo == null) {
            return null;
        }

        if (type != null && type.isEnum() && pojo.getClass() == String.class) {
            return Enum.valueOf((Class<Enum>) type, (String) pojo);
        }

        if (ReflectUtils.isPrimitives(pojo.getClass())
            && !(type != null && type.isArray() && type.getComponentType().isEnum()
                 && pojo.getClass() == String[].class)) {
            return CompatibleTypeUtils.compatibleTypeConvert(pojo, type);
        }

        Object o = history.get(pojo);

        if (o != null) {
            return o;
        }

        history.put(pojo, pojo);

        if (pojo.getClass().isArray()) {
            if (Collection.class.isAssignableFrom(type)) {
                Class<?> ctype = pojo.getClass().getComponentType();
                int len = Array.getLength(pojo);
                Collection dest = createCollection(type, len);
                history.put(pojo, dest);
                for (int i = 0; i < len; i++) {
                    Object obj = Array.get(pojo, i);
                    Object value = realize0(obj, ctype, null, history);
                    dest.add(value);
                }
                return dest;
            } else {
                Class<?> ctype = (type != null && type.isArray() ? type.getComponentType()
                    : pojo.getClass().getComponentType());
                int len = Array.getLength(pojo);
                Object dest = Array.newInstance(ctype, len);
                history.put(pojo, dest);
                for (int i = 0; i < len; i++) {
                    Object obj = Array.get(pojo, i);
                    Object value = realize0(obj, ctype, null, history);
                    Array.set(dest, i, value);
                }
                return dest;
            }
        }

        if (pojo instanceof Collection<?>) {
            if (type.isArray()) {
                Class<?> ctype = type.getComponentType();
                Collection<Object> src = (Collection<Object>) pojo;
                int len = src.size();
                Object dest = Array.newInstance(ctype, len);
                history.put(pojo, dest);
                int i = 0;
                for (Object obj : src) {
                    Object value = realize0(obj, ctype, null, history);
                    Array.set(dest, i, value);
                    i++;
                }
                return dest;
            } else {
                Collection<Object> src = (Collection<Object>) pojo;
                int len = src.size();
                Collection<Object> dest = createCollection(type, len);
                history.put(pojo, dest);
                for (Object obj : src) {
                    Type keyType = getGenericClassByIndex(genericType, 0);
                    Class<?> keyClazz = obj == null ? null : obj.getClass();
                    if (keyType instanceof Class) {
                        keyClazz = (Class<?>) keyType;
                    }
                    Object value = realize0(obj, keyClazz, keyType, history);
                    dest.add(value);
                }
                return dest;
            }
        }

        if (pojo instanceof Map<?, ?> && type != null) {
            Object className = ((Map<Object, Object>) pojo).get("class");
            if (className instanceof String) {
                try {
                    type = DefaultSerializeClassChecker.getInstance()
                        .loadClass(ClassUtils.getClassLoader(), (String) className);
                } catch (ClassNotFoundException e) {
                }
            }

            // special logic for enum
            if (type.isEnum()) {
                Object name = ((Map<Object, Object>) pojo).get("name");
                if (name != null) {
                    if (!(name instanceof String)) {
                        throw new IllegalArgumentException("`name` filed should be string!");
                    } else {
                        return Enum.valueOf((Class<Enum>) type, (String) name);
                    }
                }
            }
            Map<Object, Object> map;
            // when return type is not the subclass of return type from the signature and not an interface
            if (!type.isInterface() && !type.isAssignableFrom(pojo.getClass())) {
                try {
                    map = (Map<Object, Object>) type.newInstance();
                    Map<Object, Object> mapPojo = (Map<Object, Object>) pojo;
                    map.putAll(mapPojo);
                    if (GENERIC_WITH_CLZ) {
                        map.remove("class");
                    }
                } catch (Exception e) {
                    //ignore error
                    map = (Map<Object, Object>) pojo;
                }
            } else {
                map = (Map<Object, Object>) pojo;
            }

            if (Map.class.isAssignableFrom(type) || type == Object.class) {
                final Map<Object, Object> result;
                // fix issue#5939
                Type mapKeyType = getKeyTypeForMap(map.getClass());
                Type typeKeyType = getGenericClassByIndex(genericType, 0);
                boolean typeMismatch = mapKeyType instanceof Class && typeKeyType instanceof Class
                                       && !typeKeyType.getTypeName()
                                           .equals(mapKeyType.getTypeName());
                if (typeMismatch) {
                    result = createMap(new HashMap(0));
                } else {
                    result = createMap(map);
                }

                history.put(pojo, result);
                for (Map.Entry<Object, Object> entry : map.entrySet()) {
                    Type keyType = getGenericClassByIndex(genericType, 0);
                    Type valueType = getGenericClassByIndex(genericType, 1);
                    Class<?> keyClazz;
                    if (keyType instanceof Class) {
                        keyClazz = (Class<?>) keyType;
                    } else if (keyType instanceof ParameterizedType) {
                        keyClazz = (Class<?>) ((ParameterizedType) keyType).getRawType();
                    } else {
                        keyClazz = entry.getKey() == null ? null : entry.getKey().getClass();
                    }
                    Class<?> valueClazz;
                    if (valueType instanceof Class) {
                        valueClazz = (Class<?>) valueType;
                    } else if (valueType instanceof ParameterizedType) {
                        valueClazz = (Class<?>) ((ParameterizedType) valueType).getRawType();
                    } else {
                        valueClazz = entry.getValue() == null ? null : entry.getValue().getClass();
                    }

                    Object key = keyClazz == null ? entry.getKey()
                        : realize0(entry.getKey(), keyClazz, keyType, history);
                    Object value = valueClazz == null ? entry.getValue()
                        : realize0(entry.getValue(), valueClazz, valueType, history);
                    result.put(key, value);
                }
                return result;
            } else if (type.isInterface()) {
                Object dest = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                    new Class<?>[] { type }, new PojoInvocationHandler(map));
                history.put(pojo, dest);
                return dest;
            } else {
                Object dest;
                if (Throwable.class.isAssignableFrom(type)) {
                    Object message = map.get("message");
                    if (message instanceof String) {
                        dest = newThrowableInstance(type, (String) message);
                    } else {
                        dest = newInstance(type);
                    }
                } else {
                    dest = newInstance(type);
                }

                history.put(pojo, dest);
                for (Map.Entry<Object, Object> entry : map.entrySet()) {
                    Object key = entry.getKey();
                    if (key instanceof String) {
                        String name = (String) key;
                        Object value = entry.getValue();
                        if (value != null) {
                            Method method = getSetterMethod(dest.getClass(), name,
                                value.getClass());
                            Field field = getField(dest.getClass(), name);
                            if (method != null) {
                                if (!method.isAccessible()) {
                                    method.setAccessible(true);
                                }
                                Type ptype = method.getGenericParameterTypes()[0];
                                value = realize0(value, method.getParameterTypes()[0], ptype,
                                    history);
                                try {
                                    method.invoke(dest, value);
                                } catch (Exception e) {
                                    String exceptionDescription = "Failed to set pojo "
                                                                  + dest.getClass().getSimpleName()
                                                                  + " property " + name + " value "
                                                                  + value.getClass() + ", cause: "
                                                                  + e.getMessage();
                                    logger.error(COMMON_REFLECTIVE_OPERATION_FAILED, "", "",
                                        exceptionDescription, e);
                                    throw new RuntimeException(exceptionDescription, e);
                                }
                            } else if (field != null) {
                                value = realize0(value, field.getType(), field.getGenericType(),
                                    history);
                                try {
                                    field.set(dest, value);
                                } catch (IllegalAccessException e) {
                                    throw new RuntimeException(
                                        "Failed to set field " + name + " of pojo "
                                                               + dest.getClass().getName() + " : "
                                                               + e.getMessage(),
                                        e);
                                }
                            }
                        }
                    }
                }
                return dest;
            }
        }
        return pojo;
    }

    /**
     * Get key type for {@link Map} directly implemented by {@code clazz}.
     * If {@code clazz} does not implement {@link Map} directly, return {@code null}.
     *
     * @param clazz {@link Class}
     * @return Return String.class for {@link com.alibaba.fastjson.JSONObject}
     */
    private static Type getKeyTypeForMap(Class<?> clazz) {
        Type[] interfaces = clazz.getGenericInterfaces();
        if (!ArrayUtils.isEmpty(interfaces)) {
            for (Type type : interfaces) {
                if (type instanceof ParameterizedType) {
                    ParameterizedType t = (ParameterizedType) type;
                    if ("java.util.Map".equals(t.getRawType().getTypeName())) {
                        return t.getActualTypeArguments()[0];
                    }
                }
            }
        }
        return null;
    }

    /**
     * Get parameterized type
     *
     * @param genericType generic type
     * @param index       index of the target parameterized type
     * @return Return Person.class for List<Person>, return Person.class for Map<String, Person> when index=0
     */
    private static Type getGenericClassByIndex(Type genericType, int index) {
        Type clazz = null;
        // find parameterized type
        if (genericType instanceof ParameterizedType) {
            ParameterizedType t = (ParameterizedType) genericType;
            Type[] types = t.getActualTypeArguments();
            clazz = types[index];
        }
        return clazz;
    }

    private static Object newThrowableInstance(Class<?> cls, String message) {
        try {
            Constructor<?> messagedConstructor = cls.getDeclaredConstructor(String.class);
            return messagedConstructor.newInstance(message);
        } catch (Throwable t) {
            return newInstance(cls);
        }
    }

    private static Object newInstance(Class<?> cls) {
        try {
            return cls.newInstance();
        } catch (Throwable t) {
            Constructor<?>[] constructors = cls.getDeclaredConstructors();
            /*
              From Javadoc java.lang.Class#getDeclaredConstructors
              This method returns an array of Constructor objects reflecting all the constructors
              declared by the class represented by this Class object.
              This method returns an array of length 0,
              if this Class object represents an interface, a primitive type, an array class, or void.
             */
            if (constructors.length == 0) {
                throw new RuntimeException("Illegal constructor: " + cls.getName());
            }
            Throwable lastError = null;
            Arrays.sort(constructors, Comparator.comparingInt(a -> a.getParameterTypes().length));
            for (Constructor<?> constructor : constructors) {
                try {
                    constructor.setAccessible(true);
                    Object[] parameters = Arrays.stream(constructor.getParameterTypes())
                        .map(PojoUtils::getDefaultValue).toArray();
                    return constructor.newInstance(parameters);
                } catch (Throwable e) {
                    lastError = e;
                }
            }
            throw new RuntimeException(lastError.getMessage(), lastError);
        }
    }

    /**
     * return init value
     *
     * @param parameterType
     * @return
     */
    private static Object getDefaultValue(Class<?> parameterType) {
        if ("char".equals(parameterType.getName())) {
            return Character.MIN_VALUE;
        }
        if ("boolean".equals(parameterType.getName())) {
            return false;
        }
        if ("byte".equals(parameterType.getName())) {
            return (byte) 0;
        }
        if ("short".equals(parameterType.getName())) {
            return (short) 0;
        }
        return parameterType.isPrimitive() ? 0 : null;
    }

    private static Method getSetterMethod(Class<?> cls, String property, Class<?> valueCls) {
        String name = "set" + property.substring(0, 1).toUpperCase() + property.substring(1);
        String methodName = cls.getName() + "." + name + "(" + valueCls.getName() + ")";
        NAME_METHODS_CACHE.computeIfAbsent(cls, k -> new ConcurrentHashMap<>());
        Method method = NAME_METHODS_CACHE.get(cls).get(methodName);
        if (method == null) {
            try {
                method = cls.getMethod(name, valueCls);
            } catch (NoSuchMethodException e) {
                for (Method m : cls.getMethods()) {
                    if (ReflectUtils.isBeanPropertyWriteMethod(m) && m.getName().equals(name)) {
                        method = m;
                        break;
                    }
                }
            }
            if (method != null) {
                NAME_METHODS_CACHE.get(cls).put(methodName, method);
            }
        }
        return method;
    }

    private static Field getField(Class<?> cls, String fieldName) {
        Field result = null;
        if (CLASS_FIELD_CACHE.containsKey(cls)
            && CLASS_FIELD_CACHE.get(cls).containsKey(fieldName)) {
            return CLASS_FIELD_CACHE.get(cls).get(fieldName);
        }
        try {
            result = cls.getDeclaredField(fieldName);
            result.setAccessible(true);
        } catch (NoSuchFieldException e) {
            for (Field field : cls.getFields()) {
                if (fieldName.equals(field.getName())
                    && ReflectUtils.isPublicInstanceField(field)) {
                    result = field;
                    break;
                }
            }
        }
        if (result != null) {
            ConcurrentMap<String, Field> fields = CLASS_FIELD_CACHE.computeIfAbsent(cls,
                k -> new ConcurrentHashMap<>());
            fields.putIfAbsent(fieldName, result);
        }
        return result;
    }

    public static boolean isPojo(Class<?> cls) {
        return !ReflectUtils.isPrimitives(cls) && !Collection.class.isAssignableFrom(cls)
               && !Map.class.isAssignableFrom(cls);
    }

    /**
     * Update the property if absent
     *
     * @param getterMethod the getter method
     * @param setterMethod the setter method
     * @param newValue     the new value
     * @param <T>          the value type
     * @since 2.7.8
     */
    public static <T> void updatePropertyIfAbsent(Supplier<T> getterMethod,
                                                  Consumer<T> setterMethod, T newValue) {
        if (newValue != null && getterMethod.get() == null) {
            setterMethod.accept(newValue);
        }
    }

    /**
     * convert map to a specific class instance
     *
     * @param map map wait for convert
     * @param cls the specified class
     * @param <T> the type of {@code cls}
     * @return class instance declare in param {@code cls}
     * @throws ReflectiveOperationException if the instance creation is failed
     * @since 2.7.10
     */
    public static <T> T mapToPojo(Map<String, Object> map,
                                  Class<T> cls) throws ReflectiveOperationException {
        T instance = cls.getDeclaredConstructor().newInstance();
        Map<String, Field> beanPropertyFields = ReflectUtils.getBeanPropertyFields(cls);
        for (Map.Entry<String, Field> entry : beanPropertyFields.entrySet()) {
            String name = entry.getKey();
            Field field = entry.getValue();
            Object mapObject = map.get(name);
            if (mapObject == null) {
                continue;
            }

            Type type = field.getGenericType();
            Object fieldObject = getFieldObject(mapObject, type);
            field.set(instance, fieldObject);
        }

        return instance;
    }

    private static Object getFieldObject(Object mapObject,
                                         Type fieldType) throws ReflectiveOperationException {
        if (fieldType instanceof Class<?>) {
            return convertClassType(mapObject, (Class<?>) fieldType);
        } else if (fieldType instanceof ParameterizedType) {
            return convertParameterizedType(mapObject, (ParameterizedType) fieldType);
        } else if (fieldType instanceof GenericArrayType || fieldType instanceof TypeVariable<?>
                   || fieldType instanceof WildcardType) {
            // ignore these type currently
            return null;
        } else {
            throw new IllegalArgumentException("Unrecognized Type: " + fieldType.toString());
        }
    }

    @SuppressWarnings("unchecked")
    private static Object convertClassType(Object mapObject,
                                           Class<?> type) throws ReflectiveOperationException {
        if (type.isPrimitive() || isAssignableFrom(type, mapObject.getClass())) {
            return mapObject;
        } else if (Objects.equals(type, String.class)
                   && CLASS_CAN_BE_STRING.contains(mapObject.getClass())) {
            // auto convert specified type to string
            return mapObject.toString();
        } else if (mapObject instanceof Map) {
            return mapToPojo((Map<String, Object>) mapObject, type);
        } else {
            // type didn't match and mapObject is not another Map struct.
            // we just ignore this situation.
            return null;
        }
    }

    @SuppressWarnings("unchecked")
    private static Object convertParameterizedType(Object mapObject,
                                                   ParameterizedType type) throws ReflectiveOperationException {
        Type rawType = type.getRawType();
        if (!isAssignableFrom((Class<?>) rawType, mapObject.getClass())) {
            return null;
        }

        Type[] actualTypeArguments = type.getActualTypeArguments();
        if (isAssignableFrom(Map.class, (Class<?>) rawType)) {
            Map<Object, Object> map = (Map<Object, Object>) mapObject.getClass()
                .getDeclaredConstructor().newInstance();
            for (Map.Entry<Object, Object> entry : ((Map<Object, Object>) mapObject).entrySet()) {
                Object key = getFieldObject(entry.getKey(), actualTypeArguments[0]);
                Object value = getFieldObject(entry.getValue(), actualTypeArguments[1]);
                map.put(key, value);
            }

            return map;
        } else if (isAssignableFrom(Collection.class, (Class<?>) rawType)) {
            Collection<Object> collection = (Collection<Object>) mapObject.getClass()
                .getDeclaredConstructor().newInstance();
            for (Object m : (Iterable<?>) mapObject) {
                Object ele = getFieldObject(m, actualTypeArguments[0]);
                collection.add(ele);
            }

            return collection;
        } else {
            // ignore other type currently
            return null;
        }
    }
}